Skip to content

fix: multi-stage Dockerfile with build/dev/prod targets and ARM64 support#3335

Open
deacon-mp wants to merge 11 commits into
masterfrom
d3vco/docker-improvements
Open

fix: multi-stage Dockerfile with build/dev/prod targets and ARM64 support#3335
deacon-mp wants to merge 11 commits into
masterfrom
d3vco/docker-improvements

Conversation

@deacon-mp
Copy link
Copy Markdown
Contributor

Summary

  • Rewrites the Dockerfile into three explicit named stages: build (compiles everything), dev (mounts source via volume for local development), and prod (hardened runtime with a non-root app user)
  • Adds multi-architecture support: Node and Go are installed via direct downloads with TARGETARCH-aware selectors for amd64/arm64
  • Python virtualenv is built in the build stage and copied into dev/prod via COPY --from=build, keeping the runtime image lean
  • Sandcat agents are rebuilt during the build stage via update-agents.sh if Go is available
  • docker-compose.yml updated: removes deprecated version: key, targets the dev stage, uses slim variant by default (full offline deps are only needed in prod)
  • .dockerignore corrected to stop excluding .gitmodules (which is required for submodule initialization during the build)
  • CMD uses --insecure flag in prod stage; dev compose command uses --build

Notes / Issues to verify before merge

  • ARG GO_VERSION=1.25.4 — Go 1.25.4 did not exist as of the branch date (Nov 2025); latest stable at that time was 1.23.x. This should be verified and corrected before merge to avoid a broken build
  • The dev stage has no CMD/ENTRYPOINT; it relies entirely on docker-compose.yml providing the run command — this is intentional for the dev workflow but should be documented
  • emu's download_payloads.sh is no longer called during build; previously it was always run in the container — confirm whether this is intentional for the new flow
  • The prod stage defaults to --insecure; review whether this is appropriate for a production image

Test plan

  • Verify Go version 1.25.4 exists or update ARG GO_VERSION to a valid release
  • Build prod target: docker build --target prod .
  • Build dev target: docker build --target dev .
  • Run docker compose up and confirm Caldera starts correctly
  • Test ARM64 build: docker buildx build --platform linux/arm64 --target prod .
  • Confirm submodules are initialised correctly (.gitmodules no longer excluded from .dockerignore)
  • Confirm Sandcat agents are built correctly in the build stage

@deacon-mp deacon-mp requested a review from Copilot March 16, 2026 03:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Reworks containerization to support explicit multi-stage Docker builds (build/dev/prod), multi-arch installs (amd64/arm64), and a slimmer dev workflow via compose.

Changes:

  • Replaces single-stage runtime build with build, dev, and prod Docker stages and copies built artifacts/venv forward.
  • Adds TARGETARCH-aware Go/Node installation via direct downloads; updates compose to target dev with slim by default.
  • Adjusts build context ignores to allow submodule initialization inputs (e.g., .gitmodules).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
Dockerfile Introduces build/dev/prod stages, multi-arch Go/Node installs, venv propagation, and non-root prod user.
docker-compose.yml Targets dev stage, defaults build arg VARIANT to slim, and updates command invocation.
.dockerignore Stops excluding .gitmodules (and now also no longer excludes .git).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Dockerfile Outdated
Comment on lines +25 to +54
# Install Node
ARG TARGETARCH
ARG NODE_VERSION
RUN set -eux; \
arch="${TARGETARCH:-amd64}"; \
case "$arch" in \
amd64) node_arch="x64" ;; \
arm64) node_arch="arm64" ;; \
*) node_arch="x64" ;; \
esac; \
curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" -o /tmp/node.tar.xz; \
mkdir -p /usr/local/lib/node; \
tar -xJf /tmp/node.tar.xz -C /usr/local/lib/node --strip-components=1; \
rm -f /tmp/node.tar.xz

# Install Go
ARG GO_VERSION
RUN set -eux; \
arch="${TARGETARCH:-amd64}"; \
case "$arch" in \
amd64|arm64) \
go_arch="$arch"; \
echo "Installing Go ${GO_VERSION} for ${go_arch}"; \
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${go_arch}.tar.gz" -o /tmp/go.tgz; \
tar -C /usr/local -xzf /tmp/go.tgz; \
rm -f /tmp/go.tgz ;; \
*) \
echo "Unsupported arch ${arch}, ignoring Go install"; \
mkdir -p /usr/local/go/bin && touch /usr/local/go/bin/.install_failed ;; \
esac
Comment thread Dockerfile Outdated
Comment on lines +103 to +104
RUN (find ${APP_DIR} -type d -name ".git") | xargs rm -rf \
&& rm ${APP_DIR}/.gitmodules
Comment thread Dockerfile Outdated
Comment on lines +185 to +188
# Build VueJS front-end
RUN cd ${APP_DIR}/plugins/magma \
&& npm install \
&& npm run build
Comment thread Dockerfile Outdated
&& npm install \
&& npm run build

CMD ["python3", "/usr/src/app/server.py", "--insecure"]
Comment thread .dockerignore
docker-compose.yml
Dockerfile

# git
- Add SHA256 checksum verification for Node and Go downloads to guard
  against supply-chain / MITM issues
- Fix .git removal step: use xargs -r to handle empty find output and
  rm -f for .gitmodules to make the step idempotent
- Move VueJS UI build from prod stage to build stage so prod only
  receives compiled dist output, reducing image size and build surface
- Remove --insecure from default CMD; secure mode is now the default
  and --insecure must be passed explicitly as an opt-in
- Re-add **/.git to .dockerignore to prevent .git dirs from being sent
  in the Docker build context (submodule init uses git submodule update
  inside the build stage, not the build context's .git)
@deacon-mp deacon-mp requested a review from Copilot March 16, 2026 04:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the container build/dev workflow into explicit multi-stage Docker targets (build, dev, prod) and updates Compose defaults to use the dev target and a slimmer dependency set, while adding ARM64/AMD64-aware Node/Go installs.

Changes:

  • Replaces the single-stage-ish Docker build with build/dev/prod stages and moves compilation (venv, UI build, agents) into build.
  • Adds TARGETARCH-aware Node/Go downloads with checksum verification.
  • Updates docker-compose.yml to build the dev target by default and removes the deprecated version: key; adjusts .dockerignore to keep .gitmodules.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
Dockerfile Introduces multi-stage build (build/dev/prod), multi-arch toolchain installs, and copies built artifacts into runtime stages.
docker-compose.yml Switches default build to target: dev, sets VARIANT: slim, and updates the run command.
.dockerignore Stops excluding .gitmodules from the build context.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Dockerfile
Comment on lines +70 to +73
# Ensure plugin submodules have been cloned
RUN git config --global --add safe.directory ${APP_DIR} \
&& git submodule sync --recursive \
&& git submodule update --init --recursive
Comment thread .dockerignore
@@ -8,7 +8,6 @@ Dockerfile
**/.git
Comment thread Dockerfile
Comment on lines +75 to +79
# Install Python dependencies, allowing failed installs for plugin requirements
RUN pip install --upgrade pip \
&& sed -i '/^lxml.*/d' ${APP_DIR}/requirements.txt \
&& pip install -r ${APP_DIR}/requirements.txt \
&& find ${APP_DIR}/plugins/ -type f -name 'requirements.txt' -print0 | xargs -0 -n1 pip install --no-cache-dir -r || true
Comment thread Dockerfile
Comment on lines +75 to +80
# Install Python dependencies, allowing failed installs for plugin requirements
RUN pip install --upgrade pip \
&& sed -i '/^lxml.*/d' ${APP_DIR}/requirements.txt \
&& pip install -r ${APP_DIR}/requirements.txt \
&& find ${APP_DIR}/plugins/ -type f -name 'requirements.txt' -print0 | xargs -0 -n1 pip install --no-cache-dir -r || true

Comment thread Dockerfile
ENV PATH="${VENV_DIR}/bin:${PATH}"
ENV PATH="/usr/local/lib/node/bin:${PATH}"

CMD ["python3", "/usr/src/app/server.py"]
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the container build setup to support multi-stage Docker builds (build/dev/prod), improve runtime hardening, and enable multi-arch (amd64/arm64) builds while adjusting the default dev workflow to run via docker compose.

Changes:

  • Replaced the single-stage runtime Dockerfile with named build, dev, and prod stages and added direct Node/Go installs keyed by TARGETARCH.
  • Updated docker-compose.yml to target the dev stage and switched the default build variant to slim.
  • Updated .dockerignore to stop excluding .gitmodules from the build context.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
Dockerfile Introduces build/dev/prod stages, installs Node/Go by architecture, builds UI and agents in build stage, hardens prod runtime user.
docker-compose.yml Targets dev stage, uses slim build arg, runs server with --build, removes deprecated version: key.
.dockerignore Keeps .gitmodules in build context to support submodule-related workflows.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Dockerfile
Comment on lines +118 to +141
#----( Dev Stage )--------------------------------
FROM python:${PYTHON_VERSION}-slim-bookworm AS dev

# Set timezone (default to UTC)
ARG TZ="UTC"
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
echo $TZ > /etc/timezone

# Install pip requirements
RUN pip3 install --break-system-packages --no-cache-dir -r requirements.txt
# Install caldera dependencies
RUN apt-get update -qy \
&& apt-get --no-install-recommends -y install git curl ca-certificates unzip mingw-w64 zlib1g \
&& rm -rf /var/lib/apt/lists/*

# For offline atomic (disable it by default in slim image)
# Disable atomic if this is not downloaded
RUN if [ ! -d "/usr/src/app/plugins/atomic/data/atomic-red-team" ] && [ "$VARIANT" = "full" ]; then \
git clone --depth 1 https://github.com/redcanaryco/atomic-red-team.git \
/usr/src/app/plugins/atomic/data/atomic-red-team; \
else \
sed -i '/\- atomic/d' conf/default.yml; \
fi
COPY --from=build /usr/local/go /usr/local/go
COPY --from=build /usr/local/lib/node /usr/local/lib/node
COPY --from=build /usr/local/venv /usr/local/venv

# For offline emu
# (Emu is disabled by default, no need to disable it if slim variant is being built)
RUN if [ ! -d "/usr/src/app/plugins/emu/data/adversary-emulation-plans" ] && [ "$VARIANT" = "full" ]; then \
git clone --depth 1 https://github.com/center-for-threat-informed-defense/adversary_emulation_library \
/usr/src/app/plugins/emu/data/adversary-emulation-plans; \
fi
ENV APP_DIR=/usr/src/app
ENV VENV_DIR=/usr/local/venv
ENV PATH="/usr/local/go/bin:${PATH}"
ENV PATH="${VENV_DIR}/bin:${PATH}"
ENV PATH="/usr/local/lib/node/bin:${PATH}"

# Download emu payloads
# emu doesn't seem capable of running this itself - always download
RUN cd /usr/src/app/plugins/emu; ./download_payloads.sh
WORKDIR ${APP_DIR}
Comment thread Dockerfile
ENV PATH="${VENV_DIR}/bin:${PATH}"
ENV PATH="/usr/local/lib/node/bin:${PATH}"

CMD ["python3", "/usr/src/app/server.py"]
Comment thread Dockerfile
esac; \
curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" -o /tmp/node.tar.xz; \
curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt" -o /tmp/node.SHASUMS256.txt; \
grep "node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" /tmp/node.SHASUMS256.txt | sha256sum -c -; \
Comment thread Dockerfile
Comment on lines +70 to +73
# Ensure plugin submodules have been cloned
RUN git config --global --add safe.directory ${APP_DIR} \
&& git submodule sync --recursive \
&& git submodule update --init --recursive
Comment thread Dockerfile
Comment on lines +75 to +77
# Install Python dependencies, allowing failed installs for plugin requirements
RUN pip install --upgrade pip \
&& sed -i '/^lxml.*/d' ${APP_DIR}/requirements.txt \
Comment thread Dockerfile Outdated
Comment on lines +94 to +97
# Fetch atomic data or disable it in slim
RUN if [ "$VARIANT" = "full" ] && [ ! -d "${APP_DIR}/plugins/atomic/data/atomic-red-team" ]; then \
git clone --depth 1 https://github.com/redcanaryco/atomic-red-team.git ${APP_DIR}/plugins/atomic/data/atomic-red-team; \
else \
The else branch previously ran for both VARIANT=slim and when
VARIANT=full with an existing atomic data dir, unintentionally
removing atomic from conf/default.yml in full builds.
@deacon-mp deacon-mp requested a review from Copilot March 16, 2026 13:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR modernizes the container workflow by converting the single Dockerfile into explicit build/dev/prod stages, updating docker-compose.yml to use the dev target by default, and adjusting .dockerignore to better support submodule builds and multi-arch images.

Changes:

  • Replaces the prior Docker build flow with multi-stage build, dev, and prod targets and adds arch-aware Node/Go installation.
  • Updates docker-compose to target the dev stage and switches default build variant to slim.
  • Updates .dockerignore to stop excluding .gitmodules.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
Dockerfile Introduces multi-stage build/development/production targets, multi-arch toolchain installs, venv reuse, and a non-root prod runtime.
docker-compose.yml Targets dev stage, removes deprecated version, sets default VARIANT to slim, updates command.
.dockerignore Stops ignoring .gitmodules to support submodule-related build steps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Dockerfile
Comment on lines +70 to +73
# Ensure plugin submodules have been cloned
RUN git config --global --add safe.directory ${APP_DIR} \
&& git submodule sync --recursive \
&& git submodule update --init --recursive
Comment thread Dockerfile
ENV PATH="${VENV_DIR}/bin:$PATH"
ENV PATH="/usr/local/lib/node/bin:${PATH}"

ADD . ${APP_DIR}
Comment thread Dockerfile
Comment on lines +75 to +79
# Install Python dependencies, allowing failed installs for plugin requirements
RUN pip install --upgrade pip \
&& sed -i '/^lxml.*/d' ${APP_DIR}/requirements.txt \
&& pip install -r ${APP_DIR}/requirements.txt \
&& find ${APP_DIR}/plugins/ -type f -name 'requirements.txt' -print0 | xargs -0 -n1 pip install --no-cache-dir -r || true
Comment thread Dockerfile
ENV PATH="${VENV_DIR}/bin:${PATH}"
ENV PATH="/usr/local/lib/node/bin:${PATH}"

CMD ["python3", "/usr/src/app/server.py"]
Comment thread Dockerfile
ENV PATH="${VENV_DIR}/bin:${PATH}"
ENV PATH="/usr/local/lib/node/bin:${PATH}"

CMD ["python3", "/usr/src/app/server.py"]
Comment thread docker-compose.yml
volumes:
- ./:/usr/src/app
command: --log DEBUG
command: ["python", "/usr/src/app/server.py", "--build"]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants